/*
 *  This file is part of the Image Metadata Editor.
 *
 *  Copyright (C) 2015 Australian Institute of Marine Science
 *
 *  Contact: Gael Lafond <g.lafond@aims.org.au>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package au.gov.aims.exif.editor;

import org.apache.commons.imaging.formats.jpeg.iptc.IptcType;
import org.apache.commons.imaging.formats.jpeg.iptc.IptcTypes;
import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants;
import org.apache.commons.imaging.formats.tiff.constants.MicrosoftTagConstants;
import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo;
import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoRational;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Exif TAGS
 * All tags:
 *     http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/
 * EXIF only:
 *     http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/EXIF.html
 * XP only:
 *     http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Microsoft.html
 * See also:
 *     http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html
 */
public enum EAtlasField {
	TITLE(
		"Title:", "Image title",
		"Title", "Image title. Should be descriptive and a bit catching using words people would used to search for this image.",
		MicrosoftTagConstants.EXIF_TAG_XPTITLE,
		TiffTagConstants.TIFF_TAG_IMAGE_DESCRIPTION,
		new Tag(IptcTypes.CITY, true)
	),

	DESCRIPTION(
		"Description:", "Image description",
		"Description", "Image description. Be as descriptive as possible, provide context for the photo, what is in the photo, what does it highlight and where was it taken. Each image becomes its own web page and Google will only find it based on the description provided here.",
		MicrosoftTagConstants.EXIF_TAG_XPSUBJECT,
		IptcTypes.CAPTION_ABSTRACT
	),

	// EXIF DateTimeOriginal: Date/time when original image was taken
	// EXIF CreateDate: Called DateTimeDigitized by the EXIF spec.
	// EXIF ModifyDate: Called DateTime by the EXIF spec.
	DATE(
		"Date:", "Date when the photo was taken",
		"Date", "Date when the photo was taken",
		ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL
	),

	// EXIF TimeZoneOffset (0x882a): 1 or 2 values: 1. The time zone offset of DateTimeOriginal from GMT in hours, 2. If present, the time zone offset of ModifyDate
	// Apache library do not have that field, so we can probably ignore it
	//TIMEZONE(
	//	"Timezone:", "Timezone offset",
	//	"Timezone", "Timezone offset",
	//	ExifTagConstants.EXIF_TAG_TIMEZONE_OFFSET
	//),

	PHOTOGRAPHERS(
		"Photographers:", "The people who took the photo or made the file, separated by semi-columns.",
		"Photographers", "The people who took the photo or made the file, separated by semi-columns.",
		MicrosoftTagConstants.EXIF_TAG_XPAUTHOR,
		IptcTypes.BYLINE
	),

	ATTRIBUTION(
		"Attribution:", "Who owns the licence to the image. Can be the same as the photographers.",
		"Attribution", "Who owns the licence to the image. Can be the same as the photographers.\n" +
			"Note that the photographers will be used if no attribution is not specified.",
		IptcTypes.COPYRIGHT_NOTICE
	),

	// The licence is set in the keywords field
	LICENCE(
		"Licence:", null, // combobox have no description
		"Licence", null, // This description is dynamically generated by the method: getLicencesCsvDescription
		MicrosoftTagConstants.EXIF_TAG_XPKEYWORDS,
		IptcTypes.KEYWORDS
	),

	REASON_NOT_CC(
		"Reason for not CC:", "Reason for not licensing the photo under Creative Commons.",
		"Reason for not CC", "Reason for not licensing the photo under Creative Commons. If the photo is copyright you must provide permission for the image to be displayed on the e-Atlas.",
		MicrosoftTagConstants.EXIF_TAG_XPCOMMENT
	),

	KEYWORDS(
		"Keywords:", "List of keywords, separated by semi-columns. For e-Atlas galleries: \"GALLERY=Coral\".",
		"Keywords", "List of keywords, separated by semi-columns. All keywords which starts with \"GALLERY=\" are use by the eAtlas website to add the image into an existing image gallery.",
		MicrosoftTagConstants.EXIF_TAG_XPKEYWORDS,
		IptcTypes.KEYWORDS
	),

	LOCATION_NAME(
		"Location name:", "Name of the location where the photo was taken (if GPS is unknown).",
		"Location name", "Name of the location where the photo was taken. Use this field if you don't have GPS locations or don't wish to look up a map. We will convert the location names to Latitudes and Longitudes so be specific.",
		IptcTypes.SUBLOCATION
	),

	LONGITUDE(
		"Lon:", "Longitude (decimal)",
		"Longitude", "Longitude in decimal value",
		GpsTagConstants.GPS_TAG_GPS_LONGITUDE
	),

	LATITUDE(
		"Lat:", "Latitude (decimal)",
		"Latitude", "Latitude in decimal value",
		GpsTagConstants.GPS_TAG_GPS_LATITUDE
	),

	RADIUS(
		"Location accuracy:", "Accuracy or radius (km)",
		"Location accuracy", "Accuracy of the Latitude and Longitude fields in km. Even if the location is only approximate, say to the precision of a reef or a city then put the location and estimate the radius of the location.",
		StringUtils.GPS_TAG_GPS_H_POSITIONING_ERROR
	);

	// LinkedHashMap: HashMap that keeps insertion order
	public static final String COPYRIGHT_KEY = "copyright";
	public static final String DEFAULT_LICENCE_KEY = "cc";
	public static final Map<String, String> LICENCES = new LinkedHashMap<String, String>();
	static {
		LICENCES.put("cc", "Creative Commons Attribution 3.0 Australian Licence");
		LICENCES.put(COPYRIGHT_KEY, "Copyright - All rights reserved");
	}

	// Separator used to separate the keywords. This field has to be parsed to get
	// the image licence. The authors also use the same separator, but this application
	// do not need to parse that field.
	// Note that it is better to not change this value since it is the same as the one
	//     used by other EXIF library like the one in PHP and Windows file system.
	private static final String KEYWORDS_SEPARATOR = ";";

	// The field "reason for not using CC" is set in the image comment. Since we don't want
	// to loose the information that may already be in the comment field, a new line is added
	// with a prefix. If someone want to set the image metadata using something else than
	// this application, he will have to write that prefix manually, which may introduce
	// typo. The following list contains all form of prefix supported by the application.
	// Also, the list may contains older string, for legacy support.
	private static final String[] REASON_NOT_CC_PREFIXES = new String[]{
		"Licence extra: ", // Licence is spelt with a "c" in Australia and UK
		"License extra: "  // License is spelt with a "s" in USA
	};

	private String label;
	private String description;

	private String csvLabel;
	private String csvDescription;

	private List<Tag> tags;

	// The date format as provided by the camera (DO NOT MODIFY)
	//     http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/datetimeoriginal.html
	private static final SimpleDateFormat EXIF_DATE_FORMAT = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
	// The display date format must be understood by most spreadsheet programs (like Excel), to make it easier to use.
	private static final SimpleDateFormat DISPLAY_DATE_FORMAT = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
	// Any other date format that spreadsheet programs (like Excel) may used to output the dates.
	private static final SimpleDateFormat[] SPREADSHEET_DATE_FORMATS = new SimpleDateFormat[]{
		new SimpleDateFormat("dd/MM/yyyy HH:mm"),
		new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"),
		new SimpleDateFormat("yyyy-MM-dd HH:mm")
	};

	private EAtlasField(String label, String description, String csvLabel, String csvDescription, Object... rawTags) {
		this.label = label;
		this.description = description;
		this.csvLabel = csvLabel;
		this.csvDescription = csvDescription;

		List<Tag> tags = new ArrayList<Tag>(rawTags.length);
		for (Object rawTag : rawTags) {
			if (rawTag instanceof Tag) {
				tags.add((Tag)rawTag);
			} else if (rawTag instanceof TagInfo || rawTag instanceof IptcType) {
				tags.add(new Tag(rawTag));
			}
		}
		this.tags = tags;
	}

	/**
	 * The licence can be either "cc" (Creative Commons Attribution) or "copyright":
	 * If "copyright" is chosen, the field "Reason for not CC" has to be filled.
	 */
	private static String getLicencesCsvDescription() {
		StringBuilder sb = new StringBuilder("The licence can be either ");

		// Create an enumeration of all licence keys, separate by coma and the word "or" before the last one.
		// Example: "a", "b", "c" or "d"
		int i=0, len=LICENCES.size();
		for (String licenceKey : LICENCES.keySet()) {
			// Add a coma or the word "or" to create a proper enumeration.
			if (i > 0 && i < len) {
				sb.append(i < len-1 ? ", " : " or ");
			}
			// Add the licence abbreviation between double quotes (example: "cc")
			sb.append('\"').append(licenceKey).append('\"');
			i++;
		}
		sb.append(":\n");

		// Add a list of all licences with their description
		for (Map.Entry<String, String> licenceEntry : LICENCES.entrySet()) {
			sb.append("* ").append(licenceEntry.getKey()).append(" (").append(licenceEntry.getValue()).append(")\n");
		}
		// Add a notice
		sb.append("If \"").append(COPYRIGHT_KEY).append("\" is chosen, the field \"")
				.append(REASON_NOT_CC.getCsvLabel()).append("\" has to be filled.");
		return sb.toString();
	}

	public String getLabel() {
		return this.label;
	}
	public String getDescription() {
		return this.description;
	}

	public String getCsvLabel() {
		return this.csvLabel;
	}
	public String getCsvDescription() {
		if (this.equals(EAtlasField.LICENCE)) {
			return getLicencesCsvDescription();
		}
		return this.csvDescription;
	}

	// NOTE: GPS Value are collected using exifWrapper.getGPSValue()
	public String getValue(ExifWrapper exifWrapper) {
		if (this.equals(EAtlasField.DATE)) {

			// DATE

			String exifDateStr = this.getRawValue(exifWrapper);
			if (exifDateStr != null) {
				try {
					Date exifDate = EXIF_DATE_FORMAT.parse(exifDateStr);
					return DISPLAY_DATE_FORMAT.format(exifDate);
				} catch (ParseException e) {
					FXUtils.getNotificationWindow().showWarning("Could not parse the date found in on the photo: " +
							exifDateStr + ". The date will need to be adjusted.");
				}

				// The date could not be parsed. Return the content of the field as is
				return exifDateStr;
			}
			return null;

		} else if (this.equals(EAtlasField.LICENCE)) {

			// LICENCE

			// Get the actual value for the keywords
			String keywordsStr = this.getRawValue(exifWrapper);

			if (keywordsStr != null) {
				// Split the string into an array
				String[] keywords = keywordsStr.split(KEYWORDS_SEPARATOR);

				// Return the first "Licence" keywords
				for (String keyword : keywords) {
					keyword = keyword.trim();
					String licenceKey = getLicenceKey(keyword);
					if (licenceKey != null) {
						return licenceKey;
					}
				}
			}

			return DEFAULT_LICENCE_KEY;

		} else if (this.equals(EAtlasField.KEYWORDS)) {

			// Show all keywords, except Licence
			StringBuilder sb = new StringBuilder();
			String keywordsStr = this.getRawValue(exifWrapper);

			if (keywordsStr != null) {
				// Split the string into an array
				String[] keywords = keywordsStr.split(KEYWORDS_SEPARATOR);

				// Return the first "Licence" keywords
				for (String keyword : keywords) {
					keyword = keyword.trim();
					String licenceKey = getLicenceKey(keyword);
					if (licenceKey == null) {
						sb.append(keyword).append(KEYWORDS_SEPARATOR).append("\n");
					}
				}
			}

			return sb.toString();

		} else if (this.equals(EAtlasField.REASON_NOT_CC)) {

			// REASON FOR NOT Creative Comment

			// Get the actual value for the comment
			String commentStr = this.getRawValue(exifWrapper);

			if (commentStr != null) {
				// Normalised new lines (windows / mac => Unix)
				commentStr = commentStr.replace("\r\n", "\n").replace('\r', '\n');

				// Split the string into an array
				String[] commentLines = commentStr.split("\\n");

				// Return the first "Reason" from the comment
				for (String commentLine : commentLines) {
					commentLine = commentLine.trim();

					String reason = getReasonForNotCC(commentLine);
					if (reason != null) {
						return reason;
					}
				}
			}

			return null;

		} else if (this.equals(EAtlasField.RADIUS)) {

			// Divide the radius by 1000 (meter => km)
			String radiusStr = this.getRawValue(exifWrapper);
			if (!StringUtils.isEmpty(radiusStr)) {
				Double doubleValue = Double.valueOf(radiusStr);
				if (doubleValue != null) {
					radiusStr = Double.toString(doubleValue / 1000);
				}
			}
			return radiusStr;

		} else {

			// ALL OTHER FIELDS

			return this.getRawValue(exifWrapper);
		}
	}

	private String getRawValue(ExifWrapper exifWrapper) {
		for (Tag tag : this.tags) {
			String value = null;
			if (tag.tag instanceof TagInfoRational) {
				Double doubleValue = exifWrapper.getDoubleValue((TagInfoRational) tag.tag);
				if (doubleValue != null) {
					value = doubleValue.toString();
				}
			} else if (tag.tag instanceof TagInfo) {
				value = StringUtils.trim(exifWrapper.getExifStringValue((TagInfo) tag.tag));
			} else if (tag.tag instanceof IptcType) {
				value = StringUtils.trim(exifWrapper.getIptcValue((IptcType) tag.tag));
			}
			if (!StringUtils.isEmpty(value)) {
				return value;
			}
		}
		return null;
	}

	public void setValue(ExifWrapper exifWrapper, String value) {
		if (this.equals(EAtlasField.DATE)) {

			// DATE

			if (!StringUtils.isEmpty(value)) {
				Date exifDate;
				// Try the default date format
				try {
					exifDate = DISPLAY_DATE_FORMAT.parse(value);
				} catch (ParseException e) {
					exifDate = null;
				}
				// If the default date format failed, try other date formats until we found one that works
				if (exifDate == null) {
					for (SimpleDateFormat dateFormat : SPREADSHEET_DATE_FORMATS) {
						try {
							exifDate = dateFormat.parse(value);
						} catch (ParseException e) {
							exifDate = null;
						}
						if (exifDate != null) {
							break;
						}
					}
				}

				if (exifDate != null) {
					this.setRawValue(exifWrapper, EXIF_DATE_FORMAT.format(exifDate));
				} else {
					FXUtils.getNotificationWindow().showWarning("Invalid date format: " + value);
				}
			}

		} else if (this.equals(EAtlasField.LICENCE)) {

			// LICENCE

			StringBuilder sb = new StringBuilder();

			// Add the licence at the beginning of the new keyword string
			if (!StringUtils.isEmpty(value)) {
				sb.append(value);
			}

			// Get the actual value for the keywords
			String keywordsStr = this.getRawValue(exifWrapper);

			if (keywordsStr != null) {
				// Split the string into an array
				String[] keywords = keywordsStr.split(KEYWORDS_SEPARATOR);

				// Remove any "Licence" keywords and glue the array back to a string
				for (String keyword : keywords) {
					keyword = keyword.trim();
					if (getLicenceKey(keyword) == null) {
						if (sb.length() > 0) {
							sb.append(KEYWORDS_SEPARATOR);
						}
						sb.append(keyword);
					}
				}
			}

			this.setRawValue(exifWrapper, sb.toString());

		} else if (this.equals(EAtlasField.KEYWORDS)) {

			// The value contains all Keywords, except Licence.
			// 1. Get the value set in EXIF
			// 2. Find the Licence keyword if any
			// 3. Add it to the list
			StringBuilder sb = new StringBuilder();

			// Add all keywords set in the Keywords fields (properly formatted)
			if (!StringUtils.isEmpty(value)) {
				String[] keywords = value.split(KEYWORDS_SEPARATOR);
				for (String keyword : keywords) {
					keyword = keyword.trim();
					if (sb.length() > 0) {
						sb.append(KEYWORDS_SEPARATOR);
					}
					sb.append(keyword.trim());
				}
			}

			// Get the actual value for the keywords
			String keywordsStr = this.getRawValue(exifWrapper);

			if (keywordsStr != null) {
				// Split the string into an array
				String[] keywords = keywordsStr.split(KEYWORDS_SEPARATOR);

				// Add "Licence" keywords
				for (String keyword : keywords) {
					keyword = keyword.trim();
					if (getLicenceKey(keyword) != null) {
						if (sb.length() > 0) {
							sb.append(KEYWORDS_SEPARATOR);
						}
						sb.append(keyword);
					}
				}
			}

			this.setRawValue(exifWrapper, sb.toString());

		} else if (this.equals(EAtlasField.REASON_NOT_CC)) {

			// REASON FOR NOT Creative Comment

			StringBuilder sb = new StringBuilder();

			// Add the prefix + reason at the beginning of the new comment string
			if (!StringUtils.isEmpty(value)) {
				sb.append(REASON_NOT_CC_PREFIXES[0]).append(value);
			}

			// Get the actual value for the comments
			String commentStr = this.getRawValue(exifWrapper);

			if (commentStr != null) {
				// Normalised new lines (windows / mac => Unix)
				commentStr = commentStr.replace("\r\n", "\n").replace('\r', '\n');

				// Split the string into an array
				String[] commentLines = commentStr.split("\\n");

				// Remove any "Licence" keywords and glue the array back to a string
				for (String commentLine : commentLines) {
					commentLine = commentLine.trim();
					if (getReasonForNotCC(commentLine) == null) {
						if (sb.length() > 0) {
							sb.append("\n");
						}
						sb.append(commentLine);
					}
				}
			}

			this.setRawValue(exifWrapper, sb.toString());

		} else if (this.equals(EAtlasField.RADIUS)) {

			// Multiply the radius by 1000 (km => meter)
			String radiusStr = value;
			if (!StringUtils.isEmpty(radiusStr)) {
				Double doubleValue = Double.valueOf(radiusStr);
				if (doubleValue != null) {
					radiusStr = Double.toString(doubleValue * 1000);
				}
			}
			this.setRawValue(exifWrapper, radiusStr);

		} else {

			// ALL OTHER FIELDS

			this.setRawValue(exifWrapper, value);
		}
	}

	private void setRawValue(ExifWrapper exifWrapper, String value) {
		for (Tag tag : this.tags) {
			if (!tag.readonly) {
				if (tag.tag instanceof TagInfoRational) {
					exifWrapper.setDoubleValue(
							(TagInfoRational)tag.tag,
							StringUtils.isEmpty(value) ? null : Double.valueOf(value)
					);
				} else if (tag.tag instanceof TagInfo) {
					// EXIF tags
					exifWrapper.setExifValue((TagInfo)tag.tag, value);
				} else if (tag.tag instanceof IptcType) {
					// IPTC tags
					exifWrapper.setIptcValue((IptcType)tag.tag, value);
				}
			}
		}
	}

	/**
	 * @param keyword Keyword found in the image metadata, that may or may not be a licence keyword.
	 * @return Null is the keyword is not a licence, licence key if the keyword is one.
	 */
	private static String getLicenceKey(String keyword) {
		if (keyword == null) {
			return null;
		}
		String cleanKeyword = keyword.replaceAll("[^a-zA-Z0-9\\-_]", "");
		for (String licenceKey : LICENCES.keySet()) {
			if (licenceKey != null) {
				String cleanLicenceKey = licenceKey.replaceAll("[^a-zA-Z0-9\\-_]", "");
				if (cleanLicenceKey.equalsIgnoreCase(cleanKeyword)) {
					return licenceKey;
				}
			}
		}
		return null;
	}
	private String getReasonForNotCC(String line) {
		if (StringUtils.isEmpty(line)) {
			return null;
		}
		for (String reasonPrefix : REASON_NOT_CC_PREFIXES) {
			line = line.trim();
			reasonPrefix = reasonPrefix.trim();
			if (line.toLowerCase().startsWith(reasonPrefix.toLowerCase())) {
				return line.substring(reasonPrefix.length()).trim();
			}
		}
		return null;
	}

	private static class Tag {
		// Either TagInfo OR IptcType
		public Object tag;
		public boolean readonly;

		public Tag(Object tag) {
			this(tag, false);
		}
		public Tag(Object tag, boolean readonly) {
			this.tag = tag;
			this.readonly = readonly;
		}
	}
}
